﻿package plugins.mstandio.UniversalMap.navigation {
	
	/**
	 * MapViewer provides masked map, and dragging interface if needed
	 * it also executes updates on maps and navigation points
	 * casted form UniversalMap
	 * drag and scalling based on http://blog.des84.com/2009/04/16/drag-large-images-updated-now-with-zoom.des84
	 * @author mstandio
	 */
	
	import flash.display.Loader;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.display.Bitmap;
	import flash.events.MouseEvent;	
	import flash.ui.Mouse;
	import flash.net.URLRequest;
	import flash.events.IOErrorEvent;
	import flash.system.ApplicationDomain;
	import flash.utils.getDefinitionByName;
	import flash.utils.getQualifiedClassName;
	
	import gs.TweenLite;	
	
	import plugins.mstandio.UniversalMap.UniversalMap;
	import plugins.mstandio.UniversalMap.navigation.*;
	import plugins.mstandio.utils.combobox.ComboBox;	
		
	public class MapViewer extends Sprite {
				
		[Embed(source="../images/interface/hand_closed.png")]
			private var Bitmap_handClosed:Class;			
		[Embed(source="../images/interface/hand_opened.png")]
			private var Bitmap_handOpened:Class;				
		[Embed(source = '../images/interface/map_navigation_move.png')]
			private var Bitmap_mapNavigationMove :Class;						
		[Embed(source = '../images/interface/map_navigation_zoom.png')]
			private var Bitmap_mapNavigationZoom :Class;				
		
		
		private var mapMask:Sprite;				
		private var mapMaskWidth:Number;
		private var mapMaskHeight:Number;					

		private var mapContainer:Sprite; // contains map image and navigation points, masked by mapMask
		
		private var map:Map; // Data structure containing url of image, name and array of naviagtion points
		private var mapChanged:Boolean; // if update comes from new space on same map or from new map		
		
		private var mapImage:Loader; // External image								
		private var mapWidth:Number; // Size of external image
		private var mapHeight:Number;						
		
		private var cursor:Bitmap; // Grabbing hand cursor
		private var mouseOver:Boolean;		
		private var dragActive:Boolean;	// If grabbing hand is dragging map	
		private var normalCursorForced:Boolean; // When mouse moves over NavigationPoint
		
		private var mapNavigation:Sprite; // container for mapNavigationMove and mapNavigationZoom
		private var mapNavigationMove:Sprite; // alternative to dragging navigation that scrolls map 
		private var mapNavigationZoom:Sprite; // additional buttons for dragging
		
		private var mapNavigationSpeed:Number; // see constructor					
		private var mapNavigationZoomSpeed:Number;	
		private var mapNavigationZoomMax:Number;
				
		private var params:NavigationPointParameters; // passed down to specific NavigationPoint
		
		/**
		 * Constructor - captain obvoius strikes back
		 * @param	map
		 * @param	mapMaskWidth
		 * @param	mapMaskHeight
		 * @param	params
		 */
		public function MapViewer(map:Map, mapMaskWidth:Number, mapMaskHeight:Number, params:NavigationPointParameters) {			
			
			this.map = map;		
			
			this.mapMaskWidth = mapMaskWidth;
			this.mapMaskHeight = mapMaskHeight;						
			this.params = params;						
						
			/****hard-coded params if you wanna change them, well, good luck*****************/			
			this.mapNavigationSpeed = 15;       // Speed of scrolling with navigation arrows			
			this.mapNavigationZoomSpeed = 0.15; // Scale change after map zoom 
			this.mapNavigationZoomMax = 1.5;    // Scale for maximum zoom
			/********************************************************************************/					
						
			this.normalCursorForced = false; 									
			this.dragActive = false;			
			
			this.mapMask = new Sprite();												
			this.mapMask.graphics.beginFill(0x000000);			
			this.mapMask.graphics.drawRect(0, 0, this.mapMaskWidth, this.mapMaskHeight);			
			this.mapMask.graphics.endFill();							
			this.addChild(mapMask);																
			
			this.mapContainer = new Sprite();
			this.mapContainer.mask = mapMask;
			this.addChild(mapContainer);			
			
			this.loadMap();			
		}			
		
		/**
		 * loading map from requested url
		 */
		private function loadMap():void {		
			
			this.mapImage = new Loader();
			var request:URLRequest = new URLRequest(this.map.mapUrl);
			this.mapImage.load(request);
			this.mapImage.contentLoaderInfo.addEventListener(Event.COMPLETE, mapLoaded);			
			this.mapImage.addEventListener(IOErrorEvent.IO_ERROR, mapNotLoaded);
		}	
		
		/**
		 * When Map is loaded
		 * @param	e
		 */
		private function mapLoaded(e:Event):void {			
			this.mapImage.removeEventListener(Event.COMPLETE, mapLoaded);
			this.mapImage.removeEventListener(IOErrorEvent.IO_ERROR, mapNotLoaded);
			
			this.mapWidth = e.target.width; // size of image
			this.mapHeight = e.target.height; 			
			
			this.mapChanged = true;	
			
			this.mapContainer.addChild(this.mapImage);						
			this.determineDrag(); // if dragging interface is needed
					
			this.updateNavigationPoints(this.params); // places navigation points on map Container according to recieved params
		}	
		
		/**
		 * When map is not loaded
		 * @param	e
		 */
		private function mapNotLoaded(e:Event):void {			
			this.mapImage.removeEventListener(IOErrorEvent.IO_ERROR, mapNotLoaded);
			this.mapImage.removeEventListener(Event.COMPLETE, mapNotLoaded);
			TweenLite.killDelayedCallsTo(loadMap);
			TweenLite.delayedCall(3, loadMap);
		}
		
		/**
		 * Determines if size of map reqiures dragging interface
		 */
		private function determineDrag():void {			
			// if image size is bigger then map window
			if ((this.mapHeight > this.mapMaskHeight) || (this.mapWidth > this.mapMaskWidth)) {					
				// if dragging hasn't been activated yet
				if(dragActive == false){
					this.dragActive = true;					
					mapContainer.addEventListener(MouseEvent.ROLL_OVER, overMc);
					mapContainer.addEventListener(MouseEvent.ROLL_OUT, outMc);						
					mapContainer.addEventListener(MouseEvent.MOUSE_MOVE, moveMc);					
					mapContainer.addEventListener(MouseEvent.MOUSE_DOWN, dragMc);
					mapContainer.addEventListener(MouseEvent.MOUSE_UP, unDragMc);						
					cursor = new Bitmap(new Bitmap_handOpened().bitmapData);
					cursor.name = "cursor";
					cursor.alpha = 1 / UniversalMap.mapAlpha;
					cursor.visible = false;			
					parent.addChild(cursor);			
					if (this.mapNavigation == null) {
						this.buildMapNavigation();
					}
					this.addChild(this.mapNavigation);
					this.mapNavigationZoom.visible = this.map.mapAllowZoom;					
				}				
			}			
			// image is smaller than map window or size is equal
			else {	
				// if it hasnt been deactivated yet
				if (dragActive = true) {
					this.dragActive = false;	
					mapContainer.removeEventListener(MouseEvent.ROLL_OVER, overMc);
					mapContainer.removeEventListener(MouseEvent.ROLL_OUT, outMc);						
					mapContainer.removeEventListener(MouseEvent.MOUSE_MOVE, moveMc);					
					mapContainer.removeEventListener(MouseEvent.MOUSE_DOWN, dragMc);
					mapContainer.removeEventListener(MouseEvent.MOUSE_UP, unDragMc);						
					if (this.getChildByName("cursor") != null) {
						removeChild(getChildByName("cursor"));
					}
					if (this.getChildByName("mapNavigation") != null) {
						removeChild(getChildByName("mapNavigation"));
					}										
				}				
				this.mapContainer.x = this.mapMaskWidth * 0.5 - this.mapContainer.width * 0.5; // center map 
				this.mapContainer.y = this.mapMaskHeight* 0.5 - this.mapContainer.height * 0.5;
			}
			
			// if MapNavigation has been placed or removed then replace Combobox
			(UniversalMap(root)).placeComboBox();
		}
		
		/**
		 * Checks state of each navigation point and updates its state with params
		 * @param	params
		 */
		public function updateNavigationPoints(params:NavigationPointParameters):void {						
			if(this.mapChanged){
				// removes all navigation points and map						
				while (this.mapContainer.numChildren) {					
					if (this.mapContainer.getChildAt(0) is this.getClass(NavigationPoint)) {
						NavigationPoint(this.mapContainer.getChildAt(0)).killCam();			
					}
					this.mapContainer.removeChildAt(0);
				}			
				this.mapContainer.addChild(this.mapImage);	
		    }			
			this.params = params;						
			var somePointShowing:Boolean = false; // determines if map should scroll to the center or not
						
			for each(var navigationPoint:NavigationPoint in map.navigationPoints) { 				
				if (this.mapChanged) {								
					// adding navigation points again
					this.mapContainer.addChild(navigationPoint);
					navigationPoint.restorebeforeZoom();
			    }								
				
				if ((params.currentSpace == navigationPoint.targetSpace) || (params.currentSpace + ".preview" == navigationPoint.targetSpace) || (params.currentSpace  == navigationPoint.targetSpace + ".preview")) {
					// point represents current space
					navigationPoint.showCam(params);  					
					navigationPoint.focus();					
					somePointShowing = true;
				}else {
					// point doesnt represent current space
					if (this.mapChanged) {			
						navigationPoint.killCam();
					}else{					
						navigationPoint.hideCam();
					}
				}
			}		
			
			this.mapChanged = false; 
						
			// scroll to the center of the map, couse current map has no point representing current space
			if (!somePointShowing) {
				this.gotoXY(this.mapImage.width / 2, this.mapImage.height / 2, true);
			
			// set child index of showing point to highest value	
			}else {
				for each(navigationPoint in map.navigationPoints) { 
					if ((params.currentSpace == navigationPoint.targetSpace) || (params.currentSpace + ".preview" == navigationPoint.targetSpace) || (params.currentSpace  == navigationPoint.targetSpace + ".preview")) {
						this.mapContainer.setChildIndex(navigationPoint, this.mapContainer.numChildren - 1);
						break;
					}
				}
			}
			
			UniversalMap(root).firstShowWindow();
			
		}
		
		/**
		 * Updates MapViewer with new map and new parameters of camera for navigation point
		 * @param	map
		 * @param	params
		 */
		public function changeMap(map:Map, params:NavigationPointParameters):void {
			// removes all navigation points and map
			TweenLite.killTweensOf(this.mapContainer);				
			this.mapContainer.x = this.mapContainer.y = 0; 
			while (this.mapContainer.numChildren){
				this.mapContainer.removeChildAt(0);
			}						
			
			
			this.map = map;						
			this.params = params;									
			
			// begin map building
			this.loadMap();			
		}
				
		/**
		 * Returning to normal cursor for entering NavigationPoint
		 * casted from NavigationPoint
		 * @param	e
		 */
		public function forceNormalCursor(command:Boolean):void {
			if(this.dragActive){
				if (command) {
					this.normalCursorForced = true;									
				}else{
					this.normalCursorForced = false;				
				}
			}
		}	
		
		/**
		 * Adds mapNavigation with navigation buttons
		 */
		private function buildMapNavigation():void {
						
			this.mapNavigation = new Sprite();
			this.mapNavigation.name = "mapNavigation";
			
			// building mapNavigationMove
			this.mapNavigationMove = new Sprite();						
			var m_n:Bitmap = new Bitmap(new Bitmap_mapNavigationMove().bitmapData);
			mapNavigationMove.addChild(m_n);						
			var goLeft:Sprite = new Sprite();			
			goLeft.graphics.beginFill(0x000000,0);
			goLeft.graphics.drawRect(0, 0, 15, 15);
			goLeft.graphics.endFill();										
			goLeft.buttonMode = true;
			mapNavigationMove.addChild(goLeft);			
			var goRight:Sprite = new Sprite();
			goRight.graphics.beginFill(0x000000,0);
			goRight.graphics.drawRect(0, 0, 15, 15);
			goRight.graphics.endFill();										
			goRight.buttonMode = true;
			mapNavigationMove.addChild(goRight);			
			var goUp:Sprite = new Sprite();
			goUp.graphics.beginFill(0x000000,0);
			goUp.graphics.drawRect(0, 0, 15, 15);
			goUp.graphics.endFill();										
			goUp.buttonMode = true;
			mapNavigationMove.addChild(goUp);				
			var goDown:Sprite = new Sprite();
			goDown.graphics.beginFill(0x000000,0);
			goDown.graphics.drawRect(0, 0, 15, 15);
			goDown.graphics.endFill();										
			goDown.buttonMode = true;
			mapNavigationMove.addChild(goDown);			
			goLeft.y = m_n.height * 0.5 -  goLeft.height * 0.5;			
			goRight.y = m_n.height * 0.5 -  goLeft.height * 0.5;
			goRight.x = m_n.width - goRight.width;			
			goUp.x =  m_n.width * 0.5 -  goUp.width * 0.5;			
			goDown.x =  m_n.width * 0.5 -  goDown.width * 0.5 ;
			goDown.y =  m_n.height  - goDown.height ;			
			goLeft.addEventListener(MouseEvent.CLICK, mapNavLeft);
			goRight.addEventListener(MouseEvent.CLICK, mapNavRight);
			goUp.addEventListener(MouseEvent.CLICK, mapNavUp);
			goDown.addEventListener(MouseEvent.CLICK, mapNavDown);
			this.mapNavigationMove.x = this.mapNavigationMove.y = 2;
			
			this.mapNavigation.addChild(this.mapNavigationMove);
						
			// building mapNavigationZoom			
			this.mapNavigationZoom = new Sprite();			
			this.mapNavigationZoom.name = "mapNavigationZoom";
			var m_n_z:Bitmap = new Bitmap(new Bitmap_mapNavigationZoom().bitmapData);
			mapNavigationZoom.addChild(m_n_z);										
			var zoomIn:Sprite = new Sprite();			
			zoomIn.graphics.beginFill(0x000000,0);
			zoomIn.graphics.drawRect(0, 0, 21, 18);
			zoomIn.graphics.endFill();										
			zoomIn.buttonMode = true;
			mapNavigationZoom.addChild(zoomIn);			
			var zoomOut:Sprite = new Sprite();
			zoomOut.graphics.beginFill(0x000000,0);
			zoomOut.graphics.drawRect(0, 0, 21, 18);
			zoomOut.graphics.endFill();										
			zoomOut.buttonMode = true;
			mapNavigationZoom.addChild(zoomOut);											
			zoomIn.x =  m_n_z.width * 0.5 -  zoomIn.width * 0.5;							
			zoomOut.y =  m_n_z.height  - zoomOut.height ; 
			zoomOut.x =  m_n_z.width * 0.5 -  zoomOut.width * 0.5 ;								
			zoomIn.addEventListener(MouseEvent.CLICK, mapZoomIn);
			zoomOut.addEventListener(MouseEvent.CLICK, mapZoomOut);
			this.mapNavigationZoom.x = (this.mapNavigationMove.width +this.mapNavigationMove.x) * 0.5 - this.mapNavigationZoom.width * 0.5;
			this.mapNavigationZoom.y = this.mapNavigationMove.y + this.mapNavigationMove.height + 8; // 8 is vertical distance between mapNavigation and mapNavigationZoom
			this.mapNavigation.alpha = 1 / UniversalMap.mapAlpha;
			this.mapNavigation.addChild(mapNavigationZoom);			
		}
				
		private function mapNavLeft(e:MouseEvent):void {
			this.gotoXY(-mapContainer.x-mapNavigationSpeed, -mapContainer.y+(mapMaskHeight*0.5));
		}
		private function mapNavRight(e:MouseEvent):void {
			this.gotoXY(-mapContainer.x+mapMaskWidth+mapNavigationSpeed, -mapContainer.y+(mapMaskHeight*0.5));
		}
		private function mapNavUp(e:MouseEvent):void {
			this.gotoXY(-mapContainer.x+(mapMaskWidth*0.5), -mapContainer.y-mapNavigationSpeed);
		}
		private function mapNavDown(e:MouseEvent):void {
			this.gotoXY(-mapContainer.x+(mapMaskWidth*0.5), -mapContainer.y+mapMask.height+mapNavigationSpeed);
		}	
		
		private function mapZoomIn(e:MouseEvent):void {						
			
			var xScaleOld:Number = this.mapImage.scaleX;
			var yScaleOld:Number = this.mapImage.scaleY;
			var widthOld:Number = this.mapImage.width;			
			
			if ((this.mapImage.width * (xScaleOld + this.mapNavigationZoomSpeed)) / this.mapWidth <= this.mapNavigationZoomMax) {								
							
				this.mapImage.scaleX = (xScaleOld + this.mapNavigationZoomSpeed);			
				this.mapImage.scaleY = (yScaleOld + this.mapNavigationZoomSpeed);									
				verifyDrag(new Event(Event.ENTER_FRAME));			
				this.rescalePoints((this.mapImage.width) / widthOld);
			}			
		}
		private function mapZoomOut(e:MouseEvent):void {
			var xScaleOld:Number = this.mapImage.scaleX;
			var yScaleOld:Number = this.mapImage.scaleY;
			
			var widthOld:Number = this.mapImage.width;
			var heightOld:Number = this.mapImage.height;
			this.mapImage.scaleX = (xScaleOld - this.mapNavigationZoomSpeed);			
			this.mapImage.scaleY = (yScaleOld - this.mapNavigationZoomSpeed);			
			
			if (this.mapImage.width <= this.mapMaskWidth || this.mapImage.height <= this.mapMaskHeight ){				
				if ( this.mapImage.width <= this.mapMaskWidth ){
					this.mapImage.width = this.mapMaskWidth;
					this.mapImage.height = this.mapMaskWidth / (widthOld / heightOld);					
				}else if (this.mapImage.height <= this.mapMaskHeight) {
					this.mapImage.height = this.mapMaskHeight;
					this.mapImage.width = (widthOld / heightOld) * this.mapMaskHeight;					
				}
			}						
						
			verifyDrag(new Event(Event.ENTER_FRAME));			
			this.rescalePoints((this.mapImage.width)/widthOld);			
		}			
		
		/**
		 * Rescales all present navigation points
		 * @param ratio
		 */
		private function rescalePoints(ratio:Number):void {
			for (var i:Number = 0; i<this.mapContainer.numChildren ; i++ ) {
				this.mapContainer.getChildAt(i);
				if (this.mapContainer.getChildAt(i) is this.getClass(NavigationPoint)) {
					NavigationPoint(this.mapContainer.getChildAt(i)).rescale(ratio);			
				}
			}	
			// go to zoomed point
			this.gotoXY( ((this.mapMaskWidth * 0.5 - this.mapContainer.x) * ratio) , ((this.mapMaskHeight * 0.5 - this.mapContainer.y ) * ratio ), false); 
		}		
		
		/**
		 * Moves map to specified position
		 * @param x 
		 * @param y
		 */
		public function gotoXY(x:Number, y:Number, useTween:Boolean=true):void {				
			if (this.dragActive && x>0 && y>0 && x<= this.mapWidth && y<= this.mapHeight) {				
				var x:Number = x; 
				var y:Number = y; 			
				x = (x < this.mapMask.width * 0.5) ? this.mapMask.width * 0.5 : 
				((x > this.mapImage.width - this.mapMask.width * 0.5) ? this.mapImage.width - this.mapMask.width * 0.5 : x);
				y = (y < this.mapMask.height * 0.5) ? this.mapMask.height * 0.5 : 
				((y > this.mapImage.height - this.mapMask.height * 0.5) ? this.mapImage.height - this.mapMask.height * 0.5 : y);
				if (useTween) {
					TweenLite.to(this.mapContainer, 1, { x: - x + this.mapMaskWidth * 0.5 , y: - y  + mapMaskHeight * 0.5 } )	
				}else {
					this.mapContainer.x = - x + this.mapMaskWidth * 0.5;
					this.mapContainer.y = - y + this.mapMaskHeight * 0.5; 
				}								
			}
		}
		
				
		/**
		 *  mouse enters mapMask		 
		 */
		private function overMc(m:MouseEvent):void {
			this.cursor.x = mouseX - this.cursor.width * 0.5;
			this.cursor.y = mouseY; 
			this.mouseOver = true;
			Mouse.hide();				
			this.cursor.visible = true;			
		}
		
		/**
		 *  mouse leaves mapMask		 
		 */
		private function outMc(m:MouseEvent):void {						
			this.mouseOver = false;
			Mouse.show();
			cursor.visible = false;					
		}
		
		/**
		 * Displays dragging hand
		 */
		private function moveMc(m:MouseEvent):void {						
			this.cursor.x = mouseX - this.cursor.width * 0.5;
			this.cursor.y = mouseY; 
			if (mouseOver && !this.normalCursorForced) {
				this.cursor.visible = true;				
				Mouse.hide();				
			}else{							
				this.cursor.visible = false;			
				Mouse.show();
			}
		}						
		
		/**
		 * Starts dragginng
		 */
		private function dragMc(m:MouseEvent):void	{											
			if (!this.normalCursorForced) {				
				TweenLite.killTweensOf(this.mapContainer);				
				mapContainer.startDrag();
				mapContainer.addEventListener(Event.ENTER_FRAME, verifyDrag);
				cursor.bitmapData = new Bitmap_handClosed().bitmapData;	
				
				for each(var navigationPoint:NavigationPoint in map.navigationPoints) { 													
					if (navigationPoint.getShowing()) {
						navigationPoint.setUnFocused();						
					}					
				}						
			}
		}
		
		/**
		 * Stops dragginng
		 */
		private function unDragMc(m:MouseEvent):void {
			mapContainer.stopDrag();						
			mapContainer.removeEventListener(Event.ENTER_FRAME, verifyDrag); 
			cursor.bitmapData = new Bitmap_handOpened().bitmapData;
			if (mouseOver && !this.normalCursorForced) {
				verifyDrag(new Event(Event.ENTER_FRAME)); 												
			}						
		}				
		
		/**
		 *  Corrects location of dragged Sprite if it goes out of mapMask
		 *  also cancels dragging if clicked mouse goes out of mapMask
		 */		 
		private function verifyDrag(e:Event):void{						
			if ( mapContainer.x > mapMask.x ){ 
				mapContainer.x = mapMask.x;
			}			
			if ( mapContainer.x < (( mapMask.x + mapMask.width) - this.mapImage.width ) ){
				mapContainer.x = (( mapMask.x + mapMask.width) - this.mapImage.width );
			}			
			if ( mapContainer.y > mapMask.y ){
				mapContainer.y = mapMask.y;
			}			
			if ( mapContainer.y < (( mapMask.y + mapMask.height ) - this.mapImage.height ) ){
				mapContainer.y = (( mapMask.y + mapMask.height ) - this.mapImage.height );
			}			
			if (!mouseOver){ 				
					mapContainer.dispatchEvent(new MouseEvent(MouseEvent.MOUSE_UP));
			}
		}
		
		public function getDragActive():Boolean {
			return this.dragActive;
		}
		
		public function getNavigationWidth():Number {
			return this.mapNavigation.width;
		}		
		
		public function getClass(obj:Object):Class {
			return Class(getDefinitionByName(getQualifiedClassName(obj)));
		}
	}	
}